/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.dto.generator;

import org.eclipse.che.dto.shared.CompactJsonDto;
import org.eclipse.che.dto.shared.DTO;
import org.eclipse.che.dto.shared.DelegateTo;
import org.eclipse.che.dto.shared.JsonFieldName;
import org.eclipse.che.dto.shared.SerializationIndex;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Abstract base class for the source generating template for a single DTO. */
abstract class DtoImpl {
    protected static final String COPY_JSONS_PARAM = "copyJsons";

    private final Class<?>     dtoInterface;
    private final DtoTemplate  enclosingTemplate;
    private final boolean      compactJson;
    private final String       implClassName;
    private final List<Method> dtoMethods;

    DtoImpl(DtoTemplate enclosingTemplate, Class<?> dtoInterface) {
        this.enclosingTemplate = enclosingTemplate;
        this.dtoInterface = dtoInterface;
        this.implClassName = dtoInterface.getSimpleName() + "Impl";
        this.compactJson = DtoTemplate.implementsInterface(dtoInterface, CompactJsonDto.class);
        this.dtoMethods = ImmutableList.copyOf(calcDtoMethods());
    }

    protected boolean isCompactJson() {
        return compactJson;
    }

    public Class<?> getDtoInterface() {
        return dtoInterface;
    }

    public DtoTemplate getEnclosingTemplate() {
        return enclosingTemplate;
    }

    protected String getJavaFieldName(String getterName) {
        String fieldName;
        if (getterName.startsWith("get")) {
            fieldName = getterName.substring(3);
        } else {
            // starts with "is", see method '#ignoreMethod(Method)'
            fieldName = getterName.substring(2);
        }
        return normalizeIdentifier(Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1));
    }

    private String normalizeIdentifier(String fieldName) {
        // use $ prefix according to http://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.8
        switch (fieldName) {
            case "default":
                fieldName = "$" + fieldName;
                break;
            // add other keywords here
        }
        return fieldName;
    }

    private String getCamelCaseName(String fieldName) {
        // see normalizeIdentifier method
        if (fieldName.charAt(0) == '$') {
            fieldName = fieldName.substring(1);
        }
        return Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
    }

    protected String getImplClassName() {
        return implClassName;
    }

    protected String getSetterName(String fieldName) {
        return "set" + getCamelCaseName(fieldName);
    }

    protected String getWithName(String fieldName) {
        return "with" + getCamelCaseName(fieldName);
    }

    protected String getListAdderName(String fieldName) {
        return "add" + getCamelCaseName(fieldName);
    }

    protected String getMapPutterName(String fieldName) {
        return "put" + getCamelCaseName(fieldName);
    }

    protected String getClearName(String fieldName) {
        return "clear" + getCamelCaseName(fieldName);
    }

    protected String getEnsureName(String fieldName) {
        return "ensure" + getCamelCaseName(fieldName);
    }

    /**
     * Get the canonical name of the field by deriving it from a getter method's name.
     */
    protected String getFieldNameFromGetterName(String getterName) {
        String fieldName;
        if (getterName.startsWith("get")) {
            fieldName = getterName.substring(3);
        } else {
            // starts with "is", see method '#ignoreMethod(Method)'
            fieldName = getterName.substring(2);
        }
        return Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1);
    }

    /**
     * Get the name of the JSON field that corresponds to the given getter method in a DTO-annotated type.
     */
    protected String getJsonFieldName(Method getterMethod) {
        // First, check if a custom field name is defined for the getter
        JsonFieldName fieldNameAnn = getterMethod.getAnnotation(JsonFieldName.class);
        if (fieldNameAnn != null) {
            String customFieldName = fieldNameAnn.value();
            if (customFieldName != null && !customFieldName.isEmpty()) {
                return customFieldName;
            }
        }
        // If no custom name is given for the field, deduce it from the camel notation
        return getFieldNameFromGetterName(getterMethod.getName());
    }

    /**
     * Our super interface may implement some other interface (or not). We need to know because if it does then we need to directly extend
     * said super interfaces impl class.
     */
    protected Class<?> getSuperDtoInterface(Class<?> dto) {
        Class<?>[] superInterfaces = dto.getInterfaces();
        if (superInterfaces.length > 0) {
            for (Class<?> superInterface : superInterfaces) {
                if (superInterface.isAnnotationPresent(DTO.class)) {
                    return superInterface;
                }
            }
        }
        return null;
    }

    protected List<Method> getDtoGetters(Class<?> dto) {
        final Map<String, Method> getters = new HashMap<>();
        if (enclosingTemplate.isDtoInterface(dto)) {
            addDtoGetters(dto, getters);
            addSuperGetters(dto, getters);
        }
        return new ArrayList<>(getters.values());
    }

    /**
     * Get the names of all the getters in the super DTO interface and upper ancestors.
     */
    protected Set<String> getSuperGetterNames(Class<?> dto) {
        final Map<String, Method> getters = new HashMap<>();
        Class<?> superDto = getSuperDtoInterface(dto);
        if (superDto != null) {
            addDtoGetters(superDto, getters);
            addSuperGetters(superDto, getters);
        }
        return getters.keySet();
    }

    /**
     * Adds all getters from parent <b>NOT DTO</b> interfaces for given {@code dto} interface.
     * Does not add method when it is already present in getters map.
     */
    private void addSuperGetters(Class<?> dto, Map<String, Method> getters) {
        for (Class<?> superInterface : dto.getInterfaces()) {
            if (!superInterface.isAnnotationPresent(DTO.class)) {
                for (Method method : superInterface.getDeclaredMethods()) {
                    //when method is already present in map then child interface
                    //overrides it, which means that it should not be put into getters
                    if (isDtoGetter(method) && !getters.containsKey(method.getName())) {
                        getters.put(method.getName(), method);
                    }
                }
                addSuperGetters(superInterface, getters);
            }
        }
    }

    protected List<Method> getInheritedDtoGetters(Class<?> dto) {
        List<Method> getters = new ArrayList<>();
        if (enclosingTemplate.isDtoInterface(dto)) {
            Class<?> superInterface = getSuperDtoInterface(dto);
            while (superInterface != null) {
                addDtoGetters(superInterface, getters);
                superInterface = getSuperDtoInterface(superInterface);
            }

            addDtoGetters(dto, getters);
        }
        return getters;
    }

    private void addDtoGetters(Class<?> dto, Map<String, Method> getters) {
        for (Method method : dto.getDeclaredMethods()) {
            if (!method.isDefault() && isDtoGetter(method)) {
                getters.put(method.getName(), method);
            }
        }
    }

    private void addDtoGetters(Class<?> dto, List<Method> getters) {
        for (Method method : dto.getDeclaredMethods()) {
            if (!method.isDefault() && isDtoGetter(method)) {
                getters.add(method);
            }
        }
    }

    /** Check is specified method is DTO getter. */
    protected boolean isDtoGetter(Method method) {
        if (method.isAnnotationPresent(DelegateTo.class)) {
            return false;
        }
        String methodName = method.getName();
        if ((methodName.startsWith("get") || methodName.startsWith("is")) && method.getParameterTypes().length == 0) {
            if (methodName.length() > 2 && methodName.startsWith("is")) {
                return method.getReturnType() == Boolean.class || method.getReturnType() == boolean.class;
            }
            return methodName.length() > 3;
        }
        return false;
    }

    /** Tests whether or not a given generic type is allowed to be used as a generic. */
    protected static boolean isWhitelisted(Class<?> genericType) {
        return DtoTemplate.jreWhitelist.contains(genericType);
    }

    /** Tests whether or not a given return type is a number primitive or its wrapper type. */
    protected static boolean isNumber(Class<?> returnType) {
        if (null != returnType && (Number.class.isAssignableFrom(returnType)
                || int.class.equals(returnType) || long.class.equals(returnType)
                || short.class.equals(returnType) || float.class.equals(returnType)
                || double.class.equals(returnType) || byte.class.equals(returnType))) {
            return true;
        }
        return false;
    }

    /** Tests whether or not a given return type is a boolean primitive or its wrapper type. */
    protected static boolean isBoolean(Class<?> returnType) {
        return returnType.equals(Boolean.class) || returnType.equals(boolean.class);
    }

    protected static String getPrimitiveName(Class<?> returnType) {
        if (returnType.equals(Integer.class) || returnType.equals(int.class)) {
            return "int";
        } else if (returnType.equals(Long.class) || returnType.equals(long.class)) {
            return "long";
        } else if (returnType.equals(Short.class) || returnType.equals(short.class)) {
            return "short";
        } else if (returnType.equals(Float.class) || returnType.equals(float.class)) {
            return "float";
        } else if (returnType.equals(Double.class) || returnType.equals(double.class)) {
            return "double";
        } else if (returnType.equals(Byte.class) || returnType.equals(byte.class)) {
            return "byte";
        } else if (returnType.equals(Boolean.class) || returnType.equals(boolean.class)) {
            return "boolean";
        } else if (returnType.equals(Character.class) || returnType.equals(char.class)) {
            return "char";
        }
        throw new IllegalArgumentException("Unknown wrapper class type.");
    }

    /** Tests whether or not a given return type is a java.util.List. */
    public static boolean isList(Class<?> returnType) {
        return returnType.equals(List.class);
    }

    /** Tests whether or not a given return type is a java.util.Map. */
    public static boolean isMap(Class<?> returnType) {
        return returnType.equals(Map.class);
    }

    public static boolean isAny(Class<?> returnType) {
        return returnType.equals(Object.class);
    }

    /**
     * Expands the type and its first generic parameter (which can also have a first generic parameter (...)).
     * <p/>
     * For example, JsonArray&lt;JsonStringMap&lt;JsonArray&lt;SomeDto&gt;&gt;&gt; would produce [JsonArray, JsonStringMap, JsonArray,
     * SomeDto].
     */
    public static List<Type> expandType(Type curType) {
        List<Type> types = new LinkedList<>();
        do {
            types.add(curType);

            if (curType instanceof ParameterizedType) {
                Type[] genericParamTypes = ((ParameterizedType)curType).getActualTypeArguments();
                Type rawType = ((ParameterizedType)curType).getRawType();
                boolean map = rawType instanceof Class<?> && rawType == Map.class;
                if (!map && genericParamTypes.length != 1) {
                    throw new IllegalStateException("Multiple type parameters are not supported (neither are zero type parameters)");
                }
                Type genericParamType = map ? genericParamTypes[1] : genericParamTypes[0];
                if (genericParamType instanceof Class<?>) {
                    Class<?> genericParamTypeClass = (Class<?>)genericParamType;
                    if (isWhitelisted(genericParamTypeClass)) {
                        assert genericParamTypeClass.equals(
                                String.class) : "For JSON serialization there can be only strings or DTO types. ";
                    }
                }
                curType = genericParamType;
            } else {
                if (curType instanceof Class) {
                    Class<?> clazz = (Class<?>)curType;
                    if (isList(clazz) || isMap(clazz)) {
                        throw new DtoTemplate.MalformedDtoInterfaceException(
                                "JsonArray and JsonStringMap MUST have a generic type specified (and no... ? doesn't cut it!).");
                    }
                }
                curType = null;
            }
        } while (curType != null);
        return types;
    }

    public static Class<?> getRawClass(Type type) {
        return (Class<?>)((type instanceof ParameterizedType) ? ((ParameterizedType)type).getRawType() : type);
    }

    /**
     * Returns public methods specified in DTO interface.
     * <p/>
     * <p>For compact DTO (see {@link org.eclipse.che.dto.shared.CompactJsonDto}) methods are ordered corresponding to {@link
     * org.eclipse.che.dto.shared.SerializationIndex} annotation.
     * <p/>
     * <p>Gaps in index sequence are filled with {@code null}s.
     */
    protected List<Method> getDtoMethods() {
        return dtoMethods;
    }

    private Method[] calcDtoMethods() {
        if (!compactJson) {
            return dtoInterface.getMethods();
        }

        Map<Integer, Method> methodsMap = new HashMap<>();
        int maxIndex = 0;
        for (Method method : dtoInterface.getMethods()) {
            SerializationIndex serializationIndex = method.getAnnotation(SerializationIndex.class);
            Preconditions.checkNotNull(serializationIndex, "Serialization index is not specified for %s in %s",
                                       method.getName(), dtoInterface.getSimpleName());

            // "53" is the number of bits in JS integer.
            // This restriction will allow to add simple bit-field
            // "serialization-skipping-list" in the future.
            int index = serializationIndex.value();
            Preconditions.checkState(index > 0 && index <= 53, "Serialization index out of range [1..53] for %s in %s",
                                     method.getName(), dtoInterface.getSimpleName());

            Preconditions.checkState(!methodsMap.containsKey(index), "Duplicate serialization index for %s in %s",
                                     method.getName(), dtoInterface.getSimpleName());

            maxIndex = Math.max(index, maxIndex);
            methodsMap.put(index, method);
        }

        Method[] result = new Method[maxIndex];
        for (int index = 0; index < maxIndex; index++) {
            result[index] = methodsMap.get(index + 1);
        }

        return result;
    }

    protected boolean isLastMethod(Method method) {
        Preconditions.checkNotNull(method);
        return method == dtoMethods.get(dtoMethods.size() - 1);
    }

    /**
     * Create a textual representation of a string literal that evaluates to the given value.
     */
    protected String quoteStringLiteral(String value) {
        StringWriter sw = new StringWriter();
        try (JsonWriter writer = new JsonWriter(sw)) {
            writer.setLenient(true);
            writer.value(value);
            writer.flush();
        } catch (IOException ex) {
            throw new RuntimeException("Unexpected I/O failure: " + ex.getLocalizedMessage(), ex);
        }
        return sw.toString();
    }

    /**
     * @return String representing the source definition for the DTO impl as an inner class.
     */
    abstract String serialize();
}
